﻿using MessagePack.LZ4;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LightCAD.Core
{
    public enum AttachmentType
    {
        Text = 0,
        Binary = 1,
    }
    public class Attachment
    {
        public string StorePath = "";
        public bool NeedCompress;
        public AttachmentType Type;
        public object Body;
        public void Save(string savePath, bool onlyWithName = false)
        {
            var filePath = Path.Combine(savePath, this.StorePath);
            if (onlyWithName)
            {
                filePath = Path.Combine(savePath, Path.GetFileName(this.StorePath));
            }
            var dir = Path.GetDirectoryName(filePath);
            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }
            if (this.Type == AttachmentType.Text)
                File.WriteAllText(filePath, (string)this.Body);
            else
                File.WriteAllBytes(filePath, (byte[])this.Body);
        }
    }
    public class AttachmentPacker
    {
        public List<Attachment> Items = new List<Attachment>();
        public void AddText(string path, string text, bool needCompress = true)
        {
            var item = new Attachment
            {
                StorePath = path,
                Type = AttachmentType.Text,
                NeedCompress = needCompress,
                Body = text
            };
            Items.Add(item);
        }
        public void AddTextFile(string path, string filePath, bool needCompress = true)
        {
            AddText(path, File.ReadAllText(filePath), needCompress);
        }
        public void AddBinary(string path, byte[] data, bool needCompress = true)
        {
            var item = new Attachment
            {
                StorePath = path,
                Type = AttachmentType.Binary,
                NeedCompress = needCompress,
                Body = data
            };
            Items.Add(item);
        }
        public void AddBinaryFile(string path, string filePath, bool needCompress = true)
        {
            AddBinary(path, File.ReadAllBytes(filePath), needCompress);
        }
        public void Save(string pakFilePath)
        {
            var emptyData = new byte[16];
            for (var i = 0; i < emptyData.Length; i++)
            {
                emptyData[i] = 0;
            }
            using (var fs = new FileStream(pakFilePath, FileMode.Create))
            using (var bw = new BinaryWriter(fs))
            {
                foreach (var item in Items)
                {
                    bw.Write((int)item.Type);
                    bw.Write((int)(item.NeedCompress ? 1 : 0));
                    bw.Write(item.StorePath ?? "");

                    var objData = GetBodyData(item);
                    bw.Write(objData.Length);
                    fs.Write(objData, 0, objData.Length);

                    fs.Write(emptyData, 0, emptyData.Length);
                }

                fs.Close();
            }
        }

        public void Load(string pakFilePath)
        {
            using (var stream = new FileStream(pakFilePath, FileMode.Open))
            using (var br = new BinaryReader(stream))
            {
                while (br.PeekChar() > -1)
                {
                    var item = ReadItem(br);
                    this.Items.Add(item);
                }
                stream.Close();
            }
        }

        public List<Attachment> Finds(string path)
        {
            var items = new List<Attachment>();
            foreach (var item in this.Items)
            {
                if (item.StorePath.ToLower().StartsWith(path.ToLower()))
                {
                    items.Add(item);
                }
            }
            return items;
        }
        public void SaveItems(string savePath, List<Attachment> items)
        {
            foreach (var item in items)
            {
                item.Save(savePath);
            }
        }

        public void Extract(string pakFilePath, string extractPath)
        {
            using (var stream = new FileStream(pakFilePath, FileMode.Open))
            using (var br = new BinaryReader(stream))
            {
                while (br.PeekChar() > -1)
                {
                    var item = ReadItem(br);
                    var filePath = Path.Combine(extractPath, item.StorePath);
                    var dir = Path.GetDirectoryName(filePath);
                    if (!Directory.Exists(dir))
                    {
                        Directory.CreateDirectory(dir);
                    }
                    if (item.Type == AttachmentType.Text)
                        File.WriteAllText(filePath, (string)item.Body);
                    else
                        File.WriteAllBytes(filePath, (byte[])item.Body);
                }
                stream.Close();
            }
        }
        private Attachment ReadItem(BinaryReader br)
        {
            var type = (AttachmentType)br.ReadInt32();
            var needCompress = br.ReadInt32() == 1 ? true : false;
            var path = br.ReadString();

            var dataLen = br.ReadInt32();
            var data = br.ReadBytes(dataLen);
            br.ReadBytes(16);//empty data

            var item = new Attachment
            {
                Type = type,
                NeedCompress = needCompress,
                StorePath = path,
            };
            ParseBodyObect(item, data);
            return item;
        }
        private byte[] GetBodyData(Attachment item)
        {
            byte[] data = null;
            if (item.Type == AttachmentType.Text)
            {
                data = UTF8Encoding.UTF8.GetBytes(((string)item.Body));
            }
            else
            {
                data = (byte[])item.Body;
            }

            if (item.NeedCompress)
            {
                var input = new ReadOnlySpan<byte>(data);
                var outputData = new byte[input.Length + 512];
                var output = new Span<byte>(outputData);
                BitConverter.GetBytes(data.Length).CopyTo(outputData, 0);
                var outputLen = LZ4Codec.Encode(input, output.Slice(4));
                return output.Slice(0, outputLen + 4).ToArray();
            }
            else
            {
                return data;
            }
        }
        private void ParseBodyObect(Attachment item, byte[] data)
        {
            if (item.NeedCompress)
            {
                var input = new ReadOnlySpan<byte>(data);
                var dataSize = BitConverter.ToInt32(input.Slice(0, 4));
                var dataBuf = new Span<byte>(new byte[dataSize]);
                var decodeSize = LZ4Codec.Decode(input.Slice(4), dataBuf);
                if (dataBuf.Length != decodeSize)
                {
                    throw new Exception("lz4 decode error size");
                }
                item.Body = (item.Type == AttachmentType.Text) ? UTF8Encoding.UTF8.GetString(dataBuf) : dataBuf.ToArray();
            }
            else
            {
                item.Body = (item.Type == AttachmentType.Text) ? UTF8Encoding.UTF8.GetString(data) : data;
            }
        }
    }
}
